﻿using System;
using System.Linq;
using System.Web.Mvc;
using System.Linq.Expressions;
using System.Collections.Generic;
using Caramel;
using Caramel.Entities;
using Caramel.Web.Mvc;

namespace Caramel.Web.Mvc
{
	public static class ViewDataDictionaryExtensions
	{
		public static ViewDataDictionary AddListForModel<TModel>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember, IEnumerable<SelectListItem> items)
		{
			var modelMemberName = ViewDataDictionaryExtensions.GetExpressionText(modelMember);

			dictionary.
				Add(
					String.Format("{0}List", modelMemberName),
					items
				);

			return dictionary;
		}

		public static ViewDataDictionary AddListForModel<TModel>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember, IEnumerable<SelectListItem> items, string defaultOption)
		{
			var modelMemberName = ViewDataDictionaryExtensions.GetExpressionText(modelMember);

			dictionary.
				Add(
					String.Format("{0}List", modelMemberName),
					new[]
					{
						new SelectListItem()
						{
							Text = defaultOption,
							Selected = true
						}				
					}.
					Union(items)
				);

			return dictionary;
		}

		public static ViewDataDictionary AddNamedEntityListForModel<TModel, TEntity>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember) where TEntity : class, INamedEntity<Guid?>
		{
			return
				dictionary.
					AddListForModel<TModel>(
						modelMember,
						Repository<TEntity>.All.
							Where(e => e.IsActive).
							OrderBy(e => e.Name).
							ToList().
							ToSelectListItemList(
								e => e.Name,
								e => e.ID
							)
					);
		}

		public static ViewDataDictionary AddNamedEntityListForModel<TModel, TEntity>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember, string defaultOption) where TEntity : class, INamedEntity<Guid?>
		{
			var list =
				Repository<TEntity>.All.
					Where(e => e.IsActive).
					OrderBy(e => e.Name).
					ToList().
					ToSelectListItemList(
						e => e.Name,
						e => e.ID
					).
					ToList();

			list.
				Insert(
					0,
					new SelectListItem()
					{
						Text = defaultOption,
						Selected = true
					}
				);

			return
				dictionary.
					AddListForModel<TModel>(
						modelMember,
						list
					);
		}

		public static ViewDataDictionary AddNamedEntityListForModel<TModel, TEntity>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember, Guid? selectedID) where TEntity : class, INamedEntity<Guid?>
		{
			return
				dictionary.
					AddListForModel<TModel>(
						modelMember,
						Repository<TEntity>.All.
							Where(e => e.IsActive).
							OrderBy(e => e.Name).
							ToList().
							ToSelectListItemList(
								e => e.Name,
								e => e.ID,
								e => e.ID == selectedID
							)
					);
		}

		public static ViewDataDictionary AddNamedEntityListForModel<TModel, TEntity>(this ViewDataDictionary dictionary, Expression<Func<TModel, object>> modelMember, Guid? selectedID, string defaultOption) where TEntity : class, INamedEntity<Guid?>
		{
			var list =
				Repository<TEntity>.All.
					Where(e => e.IsActive).
					OrderBy(e => e.Name).
					ToList().
					ToSelectListItemList(
						e => e.Name,
						e => e.ID,
						e => e.ID == selectedID
					).
					ToList();

			list.
				Insert(
					0,
					new SelectListItem()
					{
						Text = defaultOption
					}
				);

			return
				dictionary.
					AddListForModel<TModel>(
						modelMember,
						list
					);
		}

		private static string GetExpressionText(LambdaExpression expression)
		{
			// Split apart the expression string for property/field accessors to create its name
			Stack<string> nameParts = new Stack<string>();
			Expression part = expression.Body;

			while (part != null)
			{
				if (part.NodeType == ExpressionType.Call)
				{
					MethodCallExpression methodExpression = (MethodCallExpression)part;

					if (!IsSingleArgumentIndexer(methodExpression))
					{
						break;
					}

					nameParts.Push(
						GetIndexerInvocation(
							methodExpression.Arguments.Single(),
							expression.Parameters.ToArray()));

					part = methodExpression.Object;
				}
				else if (part.NodeType == ExpressionType.ArrayIndex)
				{
					BinaryExpression binaryExpression = (BinaryExpression)part;

					nameParts.Push(
						GetIndexerInvocation(
							binaryExpression.Right,
							expression.Parameters.ToArray()));

					part = binaryExpression.Left;
				}
				else if (part.NodeType == ExpressionType.MemberAccess)
				{
					MemberExpression memberExpressionPart = (MemberExpression)part;
					nameParts.Push("." + memberExpressionPart.Member.Name);
					part = memberExpressionPart.Expression;
				}
				else if (part.NodeType == ExpressionType.Parameter)
				{
					// Dev10 Bug #907611
					// When the expression is parameter based (m => m.Something...), we'll push an empty
					// string onto the stack and stop evaluating. The extra empty string makes sure that
					// we don't accidentally cut off too much of m => m.Model.
					nameParts.Push(String.Empty);
					part = null;
				}
				else if (part.NodeType == ExpressionType.Convert)
				{
					UnaryExpression unaryExpression = (UnaryExpression)part;

					nameParts.Push(String.Empty);

					part = unaryExpression.Operand;
				}
				else
				{
					break;
				}
			}

			// If it starts with "model", then strip that away
			if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase))
			{
				nameParts.Pop();
			}

			if (nameParts.Count > 0)
			{
				return nameParts.Aggregate((left, right) => left + right).TrimStart('.');
			}

			return String.Empty;
		}

		private static string GetIndexerInvocation(Expression expression, ParameterExpression[] parameters)
		{
			Expression converted = Expression.Convert(expression, typeof(object));
			ParameterExpression fakeParameter = Expression.Parameter(typeof(object), null);
			Expression<Func<object, object>> lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
			Func<object, object> func = null;

			//try
			//{
			//	func = CachedExpressionCompiler.Process(lambda);
			//}
			//catch (InvalidOperationException ex)
			//{
			//	throw new InvalidOperationException(
			//		String.Format(
			//			System.Globalization.CultureInfo.CurrentCulture,
			//			"Bad",
			//			expression,
			//			parameters[0].Name),
			//		ex);
			//}

			return "[" + Convert.ToString(func(null), System.Globalization.CultureInfo.InvariantCulture) + "]";
		}

		internal static bool IsSingleArgumentIndexer(Expression expression)
		{
			MethodCallExpression methodExpression = expression as MethodCallExpression;
			if (methodExpression == null || methodExpression.Arguments.Count != 1)
			{
				return false;
			}

			return methodExpression.Method
				.DeclaringType
				.GetDefaultMembers()
				.OfType<System.Reflection.PropertyInfo>()
				.Any(p => p.GetGetMethod() == methodExpression.Method);
		}
	}
}